package com.googlecode.cswish.security;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.StrutsConstants;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.Protected;
import com.googlecode.cswish.annotation.Rest;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.security.auth.UserVisitRecord;
import com.googlecode.cswish.security.type.Calculation;
import com.googlecode.cswish.security.type.RecordRuleSetOr;
import com.googlecode.cswish.security.type.RuleSetOr;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.conversion.PageConverter;
import com.googlecode.cswish.struts.conversion.PageElementService;
import com.googlecode.cswish.struts.hibernate.ISite;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.inject.Inject;


/**
 * Path format: /xxx/simpleName.method.html
 * 
 * @author Jerry.Feng Date: 2004-10-25 18:47:59
 * @version $id: $
 */
@Service
public class SafeManager {
    /**
     * Logger for this class
     */
    private static final Log logger = LogFactory.getLog(SafeManager.class);

	public static final UserInfo ANOY_USER = new UserInfo() {	
		@Override
		public String getLoginId() {
			return "0";
		}
		@Override
		public String[] getRoleIds() {
			return null;
		}
	};
	
	private static final Permission FREE_PERMISSION = new Permission();
	private static final String REQUEST_PERMISSION_CACHE = "fr_permission_cache";
	
    private boolean PERMISSION_CHECK_ENABLE;
    
    public String warningPage;
    
    private String defaultLoginPage;
    private Wrapper<String> loginPage;
    private LoginCallback defaultLoginCallback;
    private Wrapper<LoginCallback> loginCallback;
    private Wrapper<Executor> visitCallback;
    
    private Map<String, Boolean> freePaths;
    
    @Resource
    private Config config;
    
    @Resource
    private Functions functions;
    
    @Resource
    private Routing routing;
    
    @Resource
    private BeanIntrospector beanIntrospector;
    
    @Resource
    private CacheService cacheService;
    
    @Resource
    private UserTracerService userTracerService;
    
    @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
	protected String actionExtension = "html";
    
    // TODO: dependence
    private PageConverter pageConverter;
    
    private String genericServiceName;
    
    private String defaultSite;
    
    private String UPDATE_SUFFIX;
    private String ADD_SUFFIX;
    
    /**
     * <userId, Map<pathId, check condition>>
     * 
     * Notice: Release user permission information when user logout or session destroyed
     */
    private Map<String, Map<String, Permission>> pathPermissions;
    // <UserId, UserInfo>
    private Map<String, UserInfo> userInfos;
    
    @PostConstruct
    public void init() {
    	PERMISSION_CHECK_ENABLE = config.isSecurityEnable();
    	warningPage = config.getSecurityWarningPage() + '.' + actionExtension;
    	defaultLoginPage = config.getSecurityLoginPage() + '.' + actionExtension;
    	
    	//add language
    	if (config.getLanguage() != null && config.getLanguage().equalsIgnoreCase("en")){
    		defaultLoginPage += "?request_locale=en_US";
    	}
    	defaultSite = "";
    	UPDATE_SUFFIX = ".update." + actionExtension;
    	ADD_SUFFIX = ".add." + actionExtension;
    	
    	manager = this;
    	
    	// init cache
    	loginPage = cacheService.newVar(String.class);
    	loginCallback = cacheService.newVar(LoginCallback.class);
    	visitCallback = cacheService.newVar(Executor.class);
    	Map cache = cacheService.newCache(Map.class);
    	pathPermissions = cache;
    	userInfos = cacheService.newCache(UserInfo.class);
    	freePaths = cacheService.newCache(Boolean.class);
    }
    
    private PageElementService pageElementService;
    private PageElementService getPageElementService() {
    	if (pageElementService == null) {
    		pageElementService = ObjectFactory.getInstance().getBean(PageElementService.class, true);
    	}
    	return pageElementService;
    }
    
    public void setLoginCallback(LoginCallback loginCallback) {
    	this.loginCallback.set(loginCallback);
    }
    
    public void setDefaultLoginCallback(LoginCallback loginCallback) {
    	this.defaultLoginCallback = loginCallback;
    }
    
    //private static final int FREE_PATH_LEN = FREE_PATH.length();
    //private static final int FORBIDDEN_PATH_LEN = FORBIDDEN_PATH.length();
    private static SafeManager manager;
    public static SafeManager getInstance() {
    	if (manager == null) {
    		manager = new SafeManager();
    	}
        return manager;
    }
    private SafeManager() {        
    }
    
    public Collection<String> getFreePaths() {
		return freePaths.keySet();
	}
    
    public void addFreePath(String path) {
		freePaths.put(path, Boolean.TRUE);
		
		String indexSuffix = "/index.execute." + actionExtension;
		if (path.endsWith(indexSuffix)) {
			String indexPath = path.substring(0, path.length() - indexSuffix.length()) + "/index." + actionExtension;
			freePaths.put(indexPath, Boolean.TRUE);
		}
	}

	public void removeFreePath(String path) {
		freePaths.remove(path);
	}
    
	public String getDefaultSite() {
		return defaultSite;
	}

	public void setDefaultSite(String defaultSite) {
		this.defaultSite = defaultSite;
	}

	public boolean isEnablePermissionCheck() {
    	return PERMISSION_CHECK_ENABLE;
    }
	
	public void setEnablePermissionCheck(boolean enableCheckPermission) {
		this.PERMISSION_CHECK_ENABLE = enableCheckPermission;
	}
    
    public boolean check(HttpServletRequest request, HttpServletResponse response) {
    	if (!PERMISSION_CHECK_ENABLE) {
    		if (getLoginUser(request) == null) {
    			// create login user for test
    			Invoker invoker = routing.path2Invoker(config.getSecurityLoginPage().substring(1), false);
    			String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
    			functions.service(simpleName, invoker.getSubmitMethod(), null);
    		}
    		return true;
    	}
    	
    	
        String path = Routing.getUri(request);
        
        // add the index.html automatically
        if (path != null) {
        	int index = path.length() - 1;
        	if (index > 0 && path.charAt(index) == '/') {
        		path = path + "index.html";
        	}
        }
        
        boolean pass;
        String noPermissionForwardPage = null;
    	UserInfo userInfo = getLoginUser(request);
        if (userInfo == null) {
        	if (isFreeResource(path, true)) {
        		pass = true;
        	} else {
        		pass = false;
        		
        		if (isValidPage(path)) {
        			recordLastVisitInfo(request);
            	
        			getSite();
        			noPermissionForwardPage = functions.linkNoBase(getLoginPath(), true);
        		}
        	}
        } else {
        	Permission permission = checkPageWithPermission(userInfo, path);
    		pass = permission != null;

            if (!pass) {
       			noPermissionForwardPage = functions.linkNoBase(warningPage, true);
        	}
            
            if (permission != null) {
            	request.setAttribute(REQUEST_PERMISSION_CACHE, permission);
            }
        }
         
        
        // subject is not permitted; redirect to error page
        if (!pass) {
            try {
            	if (noPermissionForwardPage != null) {
            		String queryString = request.getQueryString();
            		if (queryString != null && queryString.length() > 0) {
            			path = path + '?' + queryString;
            		}
            		
            		request.getSession().setAttribute(FrameConstant.SESSION_LAST_VISIT_INFO, path);
            		response.sendRedirect(request.getContextPath() + noPermissionForwardPage);
            		logger.info("forbid visiting page: " + path + ", redirect page to:" + noPermissionForwardPage);
            	} else {
            		if (logger.isInfoEnabled()) {
            			logger.info("forbid visiting page: " + path);
            		}
            	}
            } catch (Exception ex) {
                logger.error(ex);
            }
        }
        
        return pass;
    }
    
    private boolean isValidPage(String path) {
    	String sitePrefix = functions.getSite();
    	String indexPath = sitePrefix + "/index.html";
    	if (path.equals(indexPath)) {
    		return true;
    	}
    	
    	indexPath = sitePrefix + "/espirit/index.html";
    	if (path.equals(indexPath)) {
    		return true;
    	}
    	
    	String indexProductPath = sitePrefix + '/' + ContextHolder.get().getProductName() + "/index.html";
    	if (path.equals(indexProductPath)) {
    		return true;
    	}
    	
    	Invoker invoker = routing.path2Invoker(path, true);
    	if (pageConverter == null) {
    		pageConverter = ObjectFactory.getInstance().getBean(PageConverter.class, true);
    	}
    	String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
    	boolean validPage = pageConverter.hasPage(simpleName, invoker.methodName);

		return validPage;
	}

	private String getSite() {
    	ContextHolder context = ContextHolder.get();
    	String site = context.getSiteName();
    	if (site == null) {
    		site = ISite.DEFAULT_NAME;
    		context.setSiteName(site);
    	}
    	return site;
    }
    
    /**
     * Each context has their own login path
     * 
     * @param path
     * @return
     */
    private String getLoginPath() {
    	String loginPath = this.loginPage.get();
    	if (loginPath == null) {
    		return defaultLoginPage;
    	} else {
    		return loginPath;
    	}
    }
    
    public void redirectWarningPage(HttpServletRequest request, HttpServletResponse response) {
    	try {
    		response.sendRedirect(request.getContextPath() + warningPage);
    	} catch (Exception ex) {
            logger.error(ex);
        }	
    }
        
    public boolean login(HttpServletRequest request, String userId, String site) {
    	HttpSession session = request == null ? null : request.getSession(true);
    	return login(session, userId, site);
    }
    
    public boolean login(HttpSession session, String userId, String site) {
    	boolean online = session != null;
    	Map<String, Permission> existedPermission = pathPermissions.get(userId);
    	if (existedPermission != null && !online) {
    		return true;
    	}

    	if (site == null) {
    		site = getSite();
    	}
    	LoginCallback callback = loginCallback.get();
    	if (callback == null) {
    		callback = defaultLoginCallback;
    	}
    	UserInfo userInfo = callback.execute(this, userId, site);
    	if (userInfo != null) {
    		userInfos.put(userId, userInfo);
    		
	    	if (online) {
	        	session.setAttribute(FrameConstant.SESSION_USER_INFO + site, userInfo);
	    	}
	    	
	    	userTracerService.login(userId, online);
	    	
	    	return true;
    	} else {
    		return false;
    	}
    }
    
    public void setUserInfo(String userId, UserInfo userInfo){
    	userInfos.put(userId, userInfo);
    }
    
    
    /**
     * NOTICE:
     * this method should be called by "session filter" in order to avoid memory leak
     * 
     * @param request
     */
    public UserInfo logout(HttpServletRequest request) {
    	UserInfo userInfo = getLoginUser(request);
    	String userId = userInfo.getLoginId();
    	logout(userId);
    	
		HttpSession session = request.getSession(true);
    	session.invalidate();
    	return userInfo;
    }
    
    /**
     * logout user information
     * 
     * @param userId
     */
    public void logout(String userId) {
    	userTracerService.logout(userId);
    }
    
    // TODO: synchronization issue
    public void updatePermission(HttpServletRequest request, String userId, String site) {
    	boolean hasOldPermission = pathPermissions.remove(userId) != null;
    	
    	if (hasOldPermission) {
			if (userId != null && userId.equals(getLoginUser(request).getLoginId())) {
				login(request, userId, site);
			} else {
				login((HttpSession)null, userId, site);
			}
		}
    }
    
    public void cleanInvalidLoginUser() {
    	List<String> users = new ArrayList<String>(pathPermissions.keySet());
    	
    	for (String userId : users) {
    		Integer count = userTracerService.getOnlineCount(userId);
    		if (count == null || count < 0) {
    			userTracerService.invalid(userId);
    			pathPermissions.remove(userId);
    		} else if (count == 0) {
    			userTracerService.switchToInvalid(userId);			// remove it in the next scanning
    		}
    	}
    }

    public UserInfo getLoginUser(HttpServletRequest request) {
    	if (request == null) {
    		return null;
    	}
    	HttpSession session = request.getSession(true);
    	return getLoginUser(session);
    }
    
    public UserInfo getLoginUser(HttpSession session) {
    	UserInfo user = (UserInfo) session.getAttribute(
    			FrameConstant.SESSION_USER_INFO + ContextHolder.get().getSiteName());
    	if (user == null && !isEnablePermissionCheck()) {
    		user = ANOY_USER;
    	}
    	return user;
    }
    
    public String getLastVisitInfo(HttpServletRequest request) {
    	return (String)request.getSession().getAttribute(
    				FrameConstant.SESSION_LAST_VISIT_INFO);
    }
    
    /**
	 * @param request
	 */
	private void recordLastVisitInfo(HttpServletRequest request) {
		String skip = request.getParameter(FrameConstant.PARAM_SKIP_RECORD);
		if ("true".equals(skip)) {
			return;
		}
		
		// 1. path
		StringBuffer visitInfo = new StringBuffer(); 
		// visitInfo.append(request.getRequestURI());
		visitInfo.append(request.getServletPath());
		
		// 2. param
		StringBuffer paramInfo = new StringBuffer();
		Enumeration enu = request.getParameterNames();
		while(enu.hasMoreElements()) {
			String name = (String)enu.nextElement();
			String [] values = request.getParameterValues(name);
			if (values != null) {
				for (int i = 0; i < values.length; i++) {
					paramInfo.append('&').append(name).append('=').append(values[i]);
				}
			}
		}
		if (paramInfo.length() > 0) {
			visitInfo.append('?').append(paramInfo.substring(1));
		}
		
		// 3. cookie		
		request.getSession().setAttribute(
			FrameConstant.SESSION_LAST_VISIT_INFO,visitInfo.toString());
	}
    
    public boolean isFreeResource(String path, boolean checkMappingPath) {
		boolean pass = freePaths.containsKey(path);
		if (!pass && checkMappingPath) {
			String theMappingPath = getMappingPath(path);
			if (theMappingPath != null && !theMappingPath.equals(path)) {
				pass = freePaths.containsKey(theMappingPath);
				if (pass) {
					freePaths.put(path, true);
				}
			}
		}
		return pass;
    }
    
    private Permission getPagePermission(String userId, String path, boolean autoCreate) {
    	Map<String, Permission> pathPermission = pathPermissions.get(userId);
		
		if (pathPermission == null) {
			pathPermission = new HashMap<String, Permission>();
			pathPermissions.put(userId, pathPermission);
		}
		
		Permission permission = pathPermission.get(path);
		if (autoCreate && permission == null) {
			permission = new Permission();
			pathPermission.put(path, permission);
		}
		return permission;
    }
    
    /**
     * add page permission
     * 
     * @param userId
     * @param path
     */
	public void addPagePermission(String userId, String path) {
		addPagePermission(userId, path, Calculation.TRUE);
	}
	
	public Permission addPagePermission(String userId, String path, Calculation rule) {
		Permission permission = getPagePermission(userId, path, true);
		permission.addPathCondition(rule);
		
		// sub path extends all the permission comes from parent path
		Map<String, Permission> pathPermission = pathPermissions.get(userId);
		int index = -1;
		int lastIndex = path.length() - 1;
		// 1. get alias operations and add them to current path
		while ((index = path.indexOf('/', index + 1)) >= 0 && index < lastIndex) {
			String parentPath = path.substring(0, index + 1);
			Permission parentPermission = pathPermission.get(parentPath);
			if (parentPermission != null) {
				permission.addPathCondition(parentPermission.pathCondition);
			}
		}
		
		return permission;
	}
	
	/**
	 * Add the relevant page permission automatically when the field permission is authorized to user
	 * 
	 * @param userId
	 * @param requestId
	 * @param fieldName
	 * @param isRead
	 * @param rule
	 */
	public Permission addFieldLimit(String userId, String path,
			String fieldName, Calculation rule, boolean isRead) {
		Permission permission = getPagePermission(userId, path, false);
		if (permission == null) {
			permission = getPagePermission(userId, path, true);
			permission.setPagePermission(false);
		}
		
//		if (permission.pathCondition != null) {
//			// current path has the whole page permission
//			return;
//		}
		
		permission.addFieldCondition(fieldName, rule, isRead);
		return permission;
	}
	
	/**
	 * add record permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param rule
	 */
	public Permission addRecordLimit(String userId, String path,
			Calculation rule) {
		Permission permission = getPagePermission(userId, path, false);
		if (permission == null) {
			permission = getPagePermission(userId, path, true);
			permission.setPagePermission(false);
		}
//		if (permission.pathCondition != null) {
//			// current path has the whole record permission
//			return;
//		}
		
		permission.addRecordCondition(rule);
		return permission;
	}
	
	/**
	 * verify page permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @return
	 */
	public boolean checkPage(String userId, String path) {
		UserInfo userInfo = userInfos.get(userId);
		return checkPageWithPermission(userInfo, path) != null;
	}
	
	public Permission checkPageWithPermission(UserInfo userInfo, String path) {
		if (!isEnablePermissionCheck()) {
			return FREE_PERMISSION;
		}
		if (path == null) {
			return FREE_PERMISSION;
		}
		Permission permission = checkPage(userInfo, path, path, true, null);
		return permission;
	}
	
	public boolean checkPageField(HttpServletRequest request) {
		Permission permission = (Permission) request.getAttribute(REQUEST_PERMISSION_CACHE);
		if (permission != null) {
			Map<String, String[]> parameters = request.getParameterMap();
			String path = Routing.getUri(request);
			
			String[] type;
			if (parameters != null && (type = parameters.get(FrameConstant.PARAM_GENERIC_TYPE)) != null) {
				String classType = type[0].toString();
				if (classType.length() > 0 && !"Map".equals(classType)) {		// see ClassUtil.getRuntimeType
					String simpleName = beanIntrospector.getSimpleName(classType);
					String checkPath = functions.linkModelNoBase(simpleName, "search", true);
					UserInfo userInfo = getLoginUser(request);
					
					if (checkPage(userInfo, checkPath, checkPath, true, null) == null) {
						if (logger.isDebugEnabled()) {
							logger.debug("Failed to check permission, found illegal _type: " + classType + ", input path is " + path);
						}
						return false;
					}
				}
			}
			
			if (permission.fieldWriteCondition != null && parameters != null && !parameters.isEmpty()) {
				// FIXME: correct?
				Set<String> keySet = parameters.keySet();
				Context context = new Context(ActionContext.getContext().getValueStack(), functions);
				boolean pass = checkWriteField(permission.fieldWriteCondition, context, true, null, keySet.toArray(new String[keySet.size()]));
				if (!pass) {
					return false;
				}
			}
			
			// check the write field permission
			PageElements ps = null;
			if (path.endsWith(ADD_SUFFIX)) {
				String className = routing.path2Invoker(path, true).actionName;
				ps = getPageElementService().getPageElements(className, "_add");
			} else if (path.endsWith(UPDATE_SUFFIX)) {
				String className = routing.path2Invoker(path, true).actionName;
				ps = getPageElementService().getPageElements(className, "_update");
			}
			if (ps != null && !ps.isDisablePermissionCheck()) {
				Set<String> enableFields = ps.getAllElements().keySet();
				for (String parameterName : parameters.keySet()) {
					// don't check the hidden parameter
					if (parameterName.charAt(0) == '_') {
						continue;
					}
					
					// don't check the base type
					int index = parameterName.indexOf('.');
					if (index == -1) {
						continue;			
					}
					
					// check the nested type
					if (!enableFields.contains(parameterName)) {
						// check if the name is a[x].b.c, it can pass if there's permission 'a[0].b.c'
						index = parameterName.indexOf('[');
						if (index > 0) {
							String[] formatName = formatName(parameterName, index);
							if (enableFields.contains(formatName[0]) || enableFields.contains(formatName[1])) {
								continue;
							}
						}
						
						if (logger.isErrorEnabled()) {
							logger.error("Failed to check permission, found illegal property: " + parameterName + ", input path is " + path);
						}
						return false;
					}
				}
			}
			
			return true;
		} else {
			return true;
		}
	}
	
	private String[] formatName(String fieldName, int fromIndex) {
		StringBuilder sb = new StringBuilder(fieldName.length());
		StringBuilder sb2 = new StringBuilder(fieldName.length());
		int next = -1;
		while (fromIndex != -1) {
			String str = fieldName.substring(next + 1, fromIndex);
			sb.append(str);
			sb2.append(str);
			sb.append("[0]");
			next = fieldName.indexOf(']', fromIndex + 1);
			fromIndex = fieldName.indexOf('[', next + 1);
		}
		
		String str = fieldName.substring(next + 1);
		sb.append(str);
		sb2.append(str);
		return new String[] {sb.toString(), sb2.toString()};
	}
	
	private Permission checkPage(UserInfo userInfo, String path, String pathCacheKey, boolean checkMappingPath, Context context) {
		Permission permission = null;
		// the role has the higher priority
		if (userInfo != null && userInfo.getRoleIds() != null) {
			for (String roleId : userInfo.getRoleIds()) {
				permission = checkPageBySingleRole(false,  roleId, path, pathCacheKey, checkMappingPath, context);
				if (permission != null) {
					break;
				}
			}
		}
		if (permission == null) {
			String userId = userInfo == null ? null : userInfo.getLoginId();
			permission = checkPageBySingleRole(true, userId, path, pathCacheKey, checkMappingPath, context);
		}
		return permission;
	}
	
	private Permission checkPageBySingleRole(boolean checkFreeRes, String userId, String path, String pathCacheKey, boolean checkMappingPath, Context context) {
		if (checkFreeRes && isFreeResource(path, false)) {
			return FREE_PERMISSION;
		}
		Map<String, Permission> pathPermission = pathPermissions.get(userId);
		if (pathPermission != null) {
			int index = 0;
			String checkPath = path;
			while(index >= 0) {
				Permission permission = pathPermission.get(checkPath);
				if (permission != null && permission.isPagePermission() && (permission.pathCondition == null 
						|| permission.pathCondition.executeBoolean(context))) {
					if (checkPath != pathCacheKey) {	// check path is changed
						pathPermission.put(pathCacheKey, permission.clone());	// cache the path result
					}
					return permission;
				}
				
				// find the parent path
				boolean foundMethod = false;
				if (index == 0) { // skip the method if the method is found
					index = checkPath.lastIndexOf('.');
					if (index > 0) {
						// method ends with '.'
						index = checkPath.lastIndexOf('.', index - 1);
						foundMethod = index > 0;
					}
				}
				if (!foundMethod) {
					// skip the last path
					index = checkPath.lastIndexOf('/', checkPath.length() - 2);
				}
				
				if (index >= 0) {
					checkPath = checkPath.substring(0, index + 1);
				}
			}
		} 
		
		// continue to check the mapping path
		if (checkMappingPath) {
			String theMappingPath = getMappingPath(path);
			if (theMappingPath != null && !theMappingPath.equals(path)) {
				Permission permission = checkPageBySingleRole(true, userId, theMappingPath, pathCacheKey, false, context);
				
				if (permission != null) {
					return permission;
				}
			}
		}
		return isFreeResource(path, true) ? FREE_PERMISSION : null;
	}
	
	private String getMappingPath(String path) {
		if (path == null || path.isEmpty()) {
			return null;
		}
		
		Invoker invoker = routing.path2Invoker(path, true);
		String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
		String serviceName = beanIntrospector.getServiceName(simpleName);
		Method method = getServiceMethod(serviceName, invoker.methodName);
		if (method == null) {
			if (genericServiceName == null) {
				genericServiceName = ObjectFactory.getInstance().getBean(GenericService.class, false)
						.getClass().getSuperclass().getName();
			}
			method = getServiceMethod(genericServiceName, invoker.methodName);
		}
		
		if (method != null) {
			Rest rest = method.getAnnotation(Rest.class);
			if (rest != null) {
				String mappingServiceSimplename = rest.simpleServiceName();
				if (mappingServiceSimplename.length() == 0) {
					mappingServiceSimplename = simpleName;
				}
				String mappingMethod = rest.type();
				String theMappingPath = '/' + Routing.invoker2PathNoExtension(
						mappingServiceSimplename, invoker.separator, mappingMethod, invoker.siteName)
						+ '.' + functions.getActionExtension();
				return theMappingPath;
			} else {	
				return null;
			}
		} else {
			return null;
		}
	}
	
	private Method getServiceMethod(String serviceName, String methodName) {
		Class serviceClazz;
		try {
			serviceClazz = Class.forName(serviceName, true, Thread.currentThread().getContextClassLoader());
			Method[] methods = serviceClazz.getMethods();
			for (Method method : methods) {
				if (method.getName().equals(methodName)
						&& method.getAnnotation(Protected.class) == null) {
					return method;
				}
			}
		} catch (Throwable e) {
			if (logger.isDebugEnabled()) {
				logger.debug("Skip check the Rest permission, no mapping class found:" + serviceName + '#' + methodName);
			}
		}
		return null;
	}
	
	/**
	 * get the field name which need to control
	 * 
	 * @param userId
	 * @param path
	 * @param context
	 * @param writeLimit
	 * @return
	 */
	public List<String> listLimitedFields(String userId, String path,
			Context context, boolean writeLimit) {
		Permission permission = getPagePermission(userId, path, false);
		List<String> fieldNames = null;
		if (permission != null) {
			if (writeLimit) {
				Map<String, RuleSetOr> fieldConditions = permission.fieldWriteCondition;
				if (fieldConditions != null) {
					fieldNames = new ArrayList<String>(fieldConditions.size());
					fieldNames.addAll(fieldConditions.keySet());
				}
			} else {
				Map<String, RuleSetOr> fieldConditions = permission.fieldReadCondition;
				if (fieldConditions != null) {
					fieldNames = new ArrayList<String>(fieldConditions.size());
					for (Entry<String, RuleSetOr> entry : fieldConditions.entrySet()) {
						String fieldName = entry.getKey();
						RuleSetOr ruleSet = entry.getValue();
						String condition = ruleSet.getCondition(true);
						if (condition == null || condition.length() == 0) {
							fieldNames.add(fieldName);
						}
					}
				}
			}
		}
		return fieldNames;
	}
	
	/**
	 * list all the forbidden fields
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @param fieldNames
	 * @return
	 */
	public List<String> listForbidWriteFields(String userId, String path,
			Context context, String ... fieldNames) {
		// TODO: environment information: context
		
		List<String> fields = new ArrayList<String>();
		Permission permission = getPagePermission(userId, path, false);
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldWriteCondition;
			if (fieldConditions != null) {
				for (String fieldName : fieldNames) {
					RuleSetOr fieldPermission = fieldConditions.get(fieldName);
					if (fieldPermission != null) {
						boolean ret = fieldPermission.executeBoolean(context);
						if (!ret) {
							fields.add(fieldName);
						}
					}
				}
			}
		} else {
			if (logger.isDebugEnabled()) {
				logger.debug("User hasn't the page permission:" + path);
			}
			Collections.addAll(fields, fieldNames);
		}
		return fields;
	}
	
	/**
	 * verify field read permission
	 * 
	 * @param userId
	 * @param pathOrModel
	 * @param context
	 * @param fieldNames
	 * @return null means no limitation, else means the display value
	 */
	public String checkReadField(String userId, String pathOrModel,
			Context context, boolean executeCondition, String fullName, String fieldName) {
		// TODO: environment information: context
		Permission permission = getPagePermission(userId, pathOrModel, false);
		
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldReadCondition;
			if (fieldConditions != null) {
				RuleSetOr fieldPermission = fieldConditions.get(fullName);
				if (fieldPermission != null) {
					if (executeCondition) {
						context.setFieldName(fieldName);
						return fieldPermission.execute(context);
					}
				}
			}
		}
		return null;
	}

	/**
	 * verify field write permission
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @param fieldNames
	 * @return false if any field violated
	 */
	public boolean checkWriteField(String userId, String path,
			Context context, boolean executeCondition, String fieldPrefix, String ... fieldNames) {
		// TODO: environment information: context
		boolean violated = false;
		Permission permission = getPagePermission(userId, path, false);
		if (permission != null) {
			Map<String, RuleSetOr> fieldConditions = permission.fieldWriteCondition;
			if (fieldConditions != null && fieldNames != null && fieldNames.length > 0) {
				violated = checkWriteField(fieldConditions, context, executeCondition, fieldPrefix, fieldNames);
			}
		}
		return violated;
	}

	private boolean checkWriteField(Map<String, RuleSetOr> fieldConditions, Context context,
			boolean executeCondition, String fieldPrefix, String[] fieldNames) {
		boolean violated = false;
		
		for (String fieldName : fieldNames) {
			RuleSetOr fieldCondition = fieldConditions.get(fieldName);
			if (fieldCondition != null) {
				if (executeCondition) {
					context.setFieldName(fieldPrefix == null ? fieldName : fieldPrefix + '.' + fieldName);
					violated = fieldCondition.executeBoolean(context);
				} else {
					violated = true;
				}
				if (violated) {
					break;
				}
			}
		}
		return !violated;
	}
	
	/**
	 * get the filter condition
	 * 
	 * @param userId
	 * @param requestId
	 * @param context
	 * @return
	 */
	public RecordRuleSetOr getRecordLimitPermission(String userId, String path, Context context) {
		UserInfo userInfo = userInfos.get(userId);
		Permission permission = checkPageWithPermission(userInfo, path);
		if (permission == null) {
			return RecordRuleSetOr.FALSE;
		} else if (permission.recordCondition != null) {
			RecordRuleSetOr recordRuleSetOr = permission.recordCondition;
			recordRuleSetOr.execute(context);
			return recordRuleSetOr;
		}
		return null;
	}
	
	
	public Map<String, UserInfo> getUserInfos() {
		return MapUtils.unmodifiableMap(userInfos);
	}
	
	public static class Permission {
		
		private boolean pagePermission = true;

		private Map<String, RuleSetOr> fieldReadCondition;
		
		private Map<String, RuleSetOr> fieldWriteCondition;
		
		private RuleSetOr pathCondition;
		
		private RecordRuleSetOr recordCondition;
		
		public Permission() {
		}
		
		private void addPathCondition(Calculation condition) {
			if (pathCondition == null) {
				if (condition instanceof RuleSetOr) {
					pathCondition = (RuleSetOr) condition;
				} else {
					pathCondition = new RuleSetOr();
					pathCondition.addRule(condition);
				}
			} else {
				pathCondition.addRule(condition);
			}
			this.fieldReadCondition = null;
			this.fieldReadCondition = null;
			this.recordCondition = null;
		}
		
		public boolean isPagePermission() {
			return pagePermission;
		}

		public void setPagePermission(boolean pagePermission) {
			this.pagePermission = pagePermission;
		}

		private void addRecordCondition(Calculation condition) {
			if (recordCondition == null) {
				recordCondition = new RecordRuleSetOr();
			}
			recordCondition.addRule(condition);
		}
		
		private void addFieldCondition(String fieldName, Calculation condition, boolean isRead) {
			Map<String, RuleSetOr> fieldCondition = isRead ? fieldReadCondition : fieldWriteCondition;
			if (isRead) {
				if (fieldReadCondition == null) {
					fieldReadCondition = new HashMap<String, RuleSetOr>();
				}
				fieldCondition = fieldReadCondition;
			} else {
				if (fieldWriteCondition == null) {
					fieldWriteCondition = new HashMap<String, RuleSetOr>();
				}
				fieldCondition = fieldWriteCondition;
			}
			

			RuleSetOr fieldPermission = fieldCondition.get(fieldName);
			if (fieldPermission == null) {
				fieldPermission = new RuleSetOr();
				fieldCondition.put(fieldName, fieldPermission);
			}
			fieldPermission.addRule(condition);
		}
		
		public Permission clone() {
			Permission cloned = new Permission();
			ModelUtil.deepCopyOnFields(this, cloned);
			return cloned;
		}
	}

	public void setLoginPage(String loginPage) {
		this.loginPage.set(loginPage);
		
		String pathWithSite = functions.linkNoBase(loginPage, true);
		addFreePath(pathWithSite);
		String pathNoExtension = pathWithSite.substring(0, pathWithSite.length() - functions.getActionExtension().length() - 1);
		String submitPath = functions.getSubmitAction(pathNoExtension) + '.' + functions.getActionExtension();
		addFreePath(submitPath);
	}
	
	/**
	 * Register the user visit event
	 * 
	 * @param executor
	 */
	public void onUserVisit(Executor<Boolean, UserVisitRecord> executor) {
		visitCallback.set(executor);
	}

	/**
	 * Notify the visit event
	 * 
	 * @param userId
	 * @param ip
	 * @param uri
	 * @param spendTime
	 */
	public void notifyUserVisit(String userId, String ip, String uri, long visitTime, long spendTime) {
		Executor executor = visitCallback.get();
		if (executor != null) {
			executor.execute(new UserVisitRecord(userId, ip, uri, visitTime, spendTime));
		}
	}
}